#include <cmath>
#include <limits>

#include <QFile>
#include <QGraphicsPixmapItem>
#include <QGraphicsItemGroup>
#include <QHeaderView>
#include <QModelIndex>
#include <QTableView>
#include <QUndoStack>

#include "AddCellCommand.h"
#include "AddSheetCommand.h"
#include "ChangeSheetCommand.h"
#include "ClearCellsCommand.h"
#include "DeleteSheetCommand.h"
#include "FrakOutCell.h"
#include "FrakOutController.h"
#include "FrakOutDataItem.h"
#include "FrakOutDrawType.h"
#include "FrakOutGraphicsView.h"
#include "FrakOutScene.h"
#include "FrakOutDataTableModel.h"
#include "RemoveCellCommand.h"

FrakOutController::FrakOutController(FrakOutGraphicsView *graphicsView, QTableView *tableView, QObject *parent) :
        QObject(parent), m_currentIndex(-1), m_currentTool(FrakOutDrawType::Fill()), m_graphicsView(graphicsView),
        m_tableView(tableView), m_zoom(1)
{
    // Configure the graphics view and scene.
    m_scene = new FrakOutScene(this);
    m_scene->setItemIndexMethod(QGraphicsScene::BspTreeIndex);
    graphicsView->setScene(m_scene);

    // Configure the table.
    m_tableModel = new FrakOutDataTableModel(m_fracalcData, this);
    m_tableView->setModel(m_tableModel);
    m_tableView->verticalHeader()->setDefaultSectionSize(22);
    m_tableView->setAlternatingRowColors(true);
    m_tableView->horizontalHeader()->setResizeMode(QHeaderView::ResizeToContents);
    m_tableView->horizontalHeader()->setStretchLastSection(true);
    m_tableView->setSelectionBehavior(QAbstractItemView::SelectRows);
    m_tableView->setSelectionMode(QAbstractItemView::SingleSelection);

    // Create the undo stack.
    m_undoStack = new QUndoStack(this);

    // Establish connections.
    connect(m_graphicsView, SIGNAL(scenePositionClicked(const QPointF&)), this, SLOT(slotGraphicsViewClick(const QPointF&)));
    connect(m_graphicsView, SIGNAL(sceneZoomed(double)), this, SLOT(slotZoomLevelChanged(double)));
    connect(m_tableView, SIGNAL(clicked(const QModelIndex&)), this, SLOT(slotTableClicked(const QModelIndex&)));
}

FrakOutController::~FrakOutController()
{
    delete m_tableModel;
}

void FrakOutController::addCell(const QPointF& sceneCoordinates)
{
    double cellSize = m_fracalcData.items().at(m_currentIndex).cellSize();
    QPointF cellUpperRight = FrakOutCell::urFromRealWorld(sceneCoordinates, cellSize);
    QPair<int,int> cell(cellUpperRight.x(), cellUpperRight.y());

    if (!m_fracalcData.cellExists(m_currentIndex, cell))
    {
        // Create a rectangle graphics item and add it to the scene.
        QGraphicsRectItem *item = new QGraphicsRectItem(cellUpperRight.x() * cellSize,
                                                        cellUpperRight.y() * cellSize,
                                                        cellSize, cellSize);

        item->setBrush(QBrush(QColor(51, 204, 255, 150)));
        m_cellItemGroup->addToGroup(item);
        m_tableModel->addCellForRow(m_currentIndex, cell);
        m_undoStack->push(new AddCellCommand(tr("Add Cell"), *this, sceneCoordinates));
    }
}

void FrakOutController::addSheet(int sheetIndex, const FrakOutDataItem& sheet)
{
    if (sheetIndex == 0)
    {
        m_tableModel->addToBeginning(sheet);
    }
    else
    {
        m_tableModel->addToEnd(sheet);
    }
}

void FrakOutController::clearGroupFromScene(QGraphicsItemGroup *group)
{
    QGraphicsItem *item = 0;

    m_scene->removeItem(group);
    foreach(item, group->childItems())
    {
        group->removeFromGroup(item);
        delete item;
    }
    m_scene->addItem(group);
}

const QString& FrakOutController::currentFilename() const
{
    return m_currentFilename;
}

void FrakOutController::deleteSheet(int sheetIndex)
{
    if (m_fracalcData.numItems() > 1)
    {
        if (sheetIndex == 0)
        {
            m_tableModel->removeFromBeginning();
            setCurrentSheet(sheetIndex);
            emit signalCurrentSheetChanged(sheetIndex);
        }
        else if (sheetIndex == m_fracalcData.numItems() - 1)
        {
            m_tableModel->removeFromEnd();
            setCurrentSheet(sheetIndex - 1);
            emit signalCurrentSheetChanged(m_currentIndex);
        }
    }
}

void FrakOutController::initialiseScene()
{
    QPixmap pixmap = m_fracalcData.image();

    if (!pixmap.isNull())
    {
        QGraphicsPixmapItem *pixmapItem = new QGraphicsPixmapItem(pixmap);

        // Clear any previous scenes.
        m_scene->clear();

        // FIXME: Problem here is that the scene never shrinks... May have to wipe the scene
        // and create a new one.
        qDeleteAll( m_scene->items() );
        //m_scene->setSceneRect(QRectF());
        m_scene->setSceneRect(m_scene->itemsBoundingRect());


        // Add the image to the scene.
        m_scene->addItem(pixmapItem);

        m_gridGroup = new QGraphicsItemGroup;
        m_scene->addItem(m_gridGroup);

        m_cellItemGroup = new QGraphicsItemGroup;
        m_scene->addItem(m_cellItemGroup);
    }
}

void FrakOutController::newFromFile(const QString& filename)
{
    m_tableModel->clear();
    m_fracalcData.init(filename);
    QPixmap pixmap = m_fracalcData.image();

    if (!pixmap.isNull())
    {
        initialiseScene();

        // Create and insert a fracalc data item for the initial cell size.
        double cellSize = m_fracalcData.initialCellSize();

        m_undoStack->setClean();
        m_currentFilename = "";
        m_tableModel->addToEnd(FrakOutDataItem(cellSize));
        setCurrentSheet(0);
        emit signalCurrentSheetChanged(m_currentIndex);
    }
}

int FrakOutController::numSheets() const
{
    return m_fracalcData.numItems();
}

bool FrakOutController::openFrakOutFile(const QString& filename)
{
    QFile fracalcFile(filename);
    bool status = false;

    if ((status = fracalcFile.open(QIODevice::ReadOnly)))
    {
        QDataStream in(&fracalcFile);

        in >> m_fracalcData;
        m_undoStack->clear();
        m_currentIndex = 0;
        m_currentFilename = filename;
        initialiseScene();
        m_tableModel->updateModel();

        // Select the first item in the table and display the sheet in the scene.
        setCurrentSheet(m_currentIndex);
        emit signalCurrentSheetChanged(m_currentIndex);
    }

    return status;
}

void FrakOutController::populateSceneWithCells()
{
    const FrakOutDataItem& fracalcItem = m_fracalcData.items().at(m_currentIndex);
    double cellSize = fracalcItem.cellSize();
    QSetIterator<QPair<int,int> > cellIt(fracalcItem.cells());

    while(cellIt.hasNext())
    {
        const QPair<int,int>& cell = cellIt.peekNext();

        QGraphicsRectItem *item = new QGraphicsRectItem(cell.first * cellSize,
                                                        cell.second * cellSize,
                                                        cellSize, cellSize);
        item->setBrush(QBrush(QColor(51, 204, 255, 150)));
        m_cellItemGroup->addToGroup(item);
        (void)cellIt.next();
    }
}

void FrakOutController::populateSceneWithGrid()
{
    const FrakOutDataItem& fracalcItem = m_fracalcData.items().at(m_currentIndex);
    double cellSize = fracalcItem.cellSize();
    QPen pen(QColor(175, 175, 175, 127));
    double max = std::max(m_scene->width(), m_scene->height());
    const double MaxX = (max / cellSize) * cellSize;
    const double MaxY = MaxX;

    for (double x = 0; x <= MaxX; x += cellSize)
    {
        QGraphicsLineItem *item = new QGraphicsLineItem(x, 0, x, MaxY);
        item->setPen(pen);
        item->setZValue(std::numeric_limits<int>::min());
        m_gridGroup->addToGroup(item);
    }

    for (double y = 0; y <= MaxY; y += cellSize)
    {
        QGraphicsLineItem *item = new QGraphicsLineItem(0, y, MaxX, y);
        item->setPen(pen);
        item->setZValue(std::numeric_limits<int>::min());
        m_gridGroup->addToGroup(item);
    }
}

void FrakOutController::removeCell(const QPointF& sceneCoordinates)
{
    double cellSize = m_fracalcData.items().at(m_currentIndex).cellSize();
    QPointF cellUpperRight = FrakOutCell::urFromRealWorld(sceneCoordinates, cellSize);
    QPair<int,int> cell(cellUpperRight.x(), cellUpperRight.y());

    if (m_fracalcData.cellExists(m_currentIndex, cell))
    {
        if (QGraphicsRectItem *item = dynamic_cast<QGraphicsRectItem*>(m_scene->itemAt(sceneCoordinates)))
        {
            m_cellItemGroup->removeFromGroup(item);
            m_scene->removeItem(item);
            delete item;
        }

        m_tableModel->removeCellForRow(m_currentIndex, cell);

        // TODO: Need to fix this - the redo command will be calling remove cell.
        m_undoStack->push(new RemoveCellCommand(tr("Remove Cell"), *this, sceneCoordinates));
    }
}

void FrakOutController::removeCellsForSheet(int sheetIndex)
{
    clearGroupFromScene(m_cellItemGroup);
    m_tableModel->removeCellsForRow(sheetIndex);
}

bool FrakOutController::saveFrakOutFile(const QString& filename)
{
    QFile fracalcFile(filename);
    bool status = false;

    if ((status = fracalcFile.open(QIODevice::WriteOnly)))
    {
        QDataStream out(&fracalcFile);

        out << m_fracalcData;
        m_currentFilename = filename;
        m_undoStack->setClean();
    }

    return status;
}

QPair<int,int> FrakOutController::sceneCoordinatesToCell(const QPointF& sceneCoordinates)
{
    double cellSize = m_fracalcData.items().at(m_currentIndex).cellSize();
    QPointF cellUpperRight = FrakOutCell::urFromRealWorld(sceneCoordinates, cellSize);
    QPair<int,int> cell(cellUpperRight.x(), cellUpperRight.y());

    return cell;
}

void FrakOutController::setCellsForSheet(int sheetIndex, const QSet<QPair<int,int> >& cells)
{
    m_tableModel->setCellsForRow(sheetIndex, cells);
    clearGroupFromScene(m_gridGroup);
    populateSceneWithGrid();
    clearGroupFromScene(m_cellItemGroup);
    populateSceneWithCells();
}

void FrakOutController::setCurrentSheet(int sheetIndex)
{
    m_currentIndex = sheetIndex;

    // Select the item in the table view.
    m_tableView->setCurrentIndex(m_tableModel->index(sheetIndex, 0));
    clearGroupFromScene(m_gridGroup);
    clearGroupFromScene(m_cellItemGroup);
    m_currentIndex = sheetIndex;
    populateSceneWithGrid();
    populateSceneWithCells();
    emit signalCurrentSheetChanged(m_currentIndex);
}

void FrakOutController::setTool(const FrakOutDrawType* const drawType)
{
    m_currentTool = drawType;
}

void FrakOutController::setZoomLevel(double zoom)
{
    if (zoom >= 0.1 && zoom <= 5)
    {
        m_zoom = zoom;
        m_graphicsView->slotZoom(zoom);
    }
}

void FrakOutController::slotAddItemBeginning()
{
    if (m_fracalcData.numItems() > 0)
    {
        double cellSize = m_fracalcData.items().at(0).cellSize() * m_fracalcData.cellScaleFactor();

        if (cellSize < (m_scene->width() * 0.5))
        {
            m_undoStack->push(new AddSheetCommand(tr("Add Sheet"), *this, FrakOutDataItem(cellSize), 0));
        }
    }
}

void FrakOutController::slotAddItemEnd()
{
    int numItems = m_fracalcData.numItems();

    if (numItems > 0)
    {
        double cellSize = m_fracalcData.items().at(numItems - 1).cellSize() * (1.0 / m_fracalcData.cellScaleFactor());

        if (cellSize >= 2)
        {
            m_undoStack->push(new AddSheetCommand(tr("Add Sheet"), *this, FrakOutDataItem(cellSize), numItems));
        }
    }
}

void FrakOutController::slotClearCellsFromCurrentSheet()
{
    const FrakOutDataItem& sheet = m_fracalcData.items().at(m_currentIndex);
    m_undoStack->push(new ClearCellsCommand(tr("Erase Cells"), *this, sheet, m_currentIndex));
}

void FrakOutController::slotDeleteSheet()
{
    const FrakOutDataItem& sheet = m_fracalcData.items().at(m_currentIndex);

    // This calls FrakOutController::deleteSheet via the redo method.
    m_undoStack->push(new DeleteSheetCommand(tr("Delete Sheet"), *this, sheet, m_currentIndex));
}

void FrakOutController::slotGraphicsViewClick(const QPointF& sceneCoordinates)
{
    if (m_fracalcData.numItems() > 0)
    {
        QPair<int,int> cell = sceneCoordinatesToCell(sceneCoordinates);

        if (m_currentTool == FrakOutDrawType::Fill())
        {
            addCell(sceneCoordinates);
        }
        else if (m_currentTool == FrakOutDrawType::Erase())
        {
            removeCell(sceneCoordinates);
        }
    }
}

void FrakOutController::slotZoomLevelChanged(double zoomInc)
{
    setZoomLevel(m_zoom + zoomInc);
}

void FrakOutController::slotTableClicked(const QModelIndex& index)
{
    // This ultimately calls the FrakOutController::changeSheet method.
    m_undoStack->push(new ChangeSheetCommand(tr("Change Sheet"), *this, m_currentIndex, index.row()));
}

QUndoStack* FrakOutController::undoStack() const
{
    return m_undoStack;
}

double FrakOutController::zoomLevel() const
{
    return m_zoom;
}
